package git.soulbgm.common.aspect;

import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.LinkedHashMap;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import git.soulbgm.common.anntation.Log;
import git.soulbgm.common.enums.OperationType;
import git.soulbgm.common.pojo.ResultData;
import git.soulbgm.common.pojo.ReturnCode;
import git.soulbgm.common.pojo.TableDataInfo;
import git.soulbgm.pojo.LogInfo;
import git.soulbgm.pojo.UserInfo;
import git.soulbgm.service.LogInfoService;
import git.soulbgm.utils.IpUtil;
import git.soulbgm.utils.ServletUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.stereotype.Component;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

/**
 * 日志切面
 *
 * @author SoulBGM
 * @date 2022/11/18 15:13
 */
@Aspect
@Component
public class LogAspect {

    private final ObjectMapper objectMapper;
    private final LogInfoService logInfoService;

    /**
     * 日期格式
     */
    private final static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    /**
     * 调用符号
     */
    private final static String CALL_SYMBOL = ".";

    public LogAspect(LogInfoService logInfoService) {
        this.objectMapper = new ObjectMapper();
        // 忽略value为null 时 key的输出
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        // 设置Date输出格式
        objectMapper.setDateFormat(DATE_FORMAT);
        this.logInfoService = logInfoService;
    }

    /**
     * 对带有 @Log 注解的方法进行切面
     *
     * @param pjp 进行连接点
     * @param log @Log注解
     * @return {@link Object}
     * @throws Throwable 异常
     */
    @Around(value = "@annotation(log)", argNames = "pjp, log")
    public Object around(ProceedingJoinPoint pjp, Log log) throws Throwable {
        UserDto userDto = getUserDto();
        long beginTime = System.currentTimeMillis();
        Object result = pjp.proceed();
        long timeConsuming = System.currentTimeMillis() - beginTime;

        try {
            saveLogInfo(pjp, log, userDto, timeConsuming, result);
        } catch (Exception e) {
            e.printStackTrace();
        }

        return result;
    }

    /**
     * 获取用户DTO
     *
     * @return {@link UserDto}
     */
    private UserDto getUserDto() {
        UserDto userDto = null;
        /* SpringSecurity 获取用户方式
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            Object principal = authentication.getPrincipal();
            if (principal instanceof LoginUser) {
                LoginUser loginUser = (LoginUser) principal;
                userDto = new UserDto(loginUser.getId, loginUser.getNickname());
            }
        }*/
        UserInfo userInfo = (UserInfo) ServletUtils.getSession().getAttribute("user");
        if (userInfo != null) {
            userDto = new UserDto(userInfo.getId(), userInfo.getNickname());
        }
        return userDto == null ? new UserDto() : userDto;
    }

    /**
     * 本地变量表参数名称发现器
     */
    private final static LocalVariableTableParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new LocalVariableTableParameterNameDiscoverer();

    /**
     * SpEL解析器
     */
    private final static ExpressionParser SP_EL_PARSER = new SpelExpressionParser();

    /**
     * 保存日志信息
     *
     * @param pjp           ProceedingJoinPoint对象
     * @param log           @Log注解
     * @param userDto       用户DTO
     * @param timeConsuming 耗时
     * @param result        结果
     * @throws JsonProcessingException Json处理异常
     */
    private void saveLogInfo(ProceedingJoinPoint pjp, Log log, UserDto userDto, long timeConsuming, Object result) throws JsonProcessingException {
        // 获取request
        HttpServletRequest request = ServletUtils.getRequest();
        // 请求路径
        String requestUrl = request.getRequestURI();
        // 请求方法
        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
        //参数
        String[] paramNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
        paramNames = paramNames == null ? new String[0] : paramNames;
        // 请求的方法参数值
        Object[] args = pjp.getArgs();
        // 操作IP
        String ip = IpUtil.getIp(request);
        // 操作类型
        OperationType operationType = log.type();

        if (userDto.getUserId() == 0L) {
            userDto = getUserDto();
        }

        LogInfo logInfo = new LogInfo();
        logInfo.setUserId(userDto.getUserId());
        logInfo.setNickname(userDto.getNickname());
        logInfo.setOperationIp(ip);
        logInfo.setType(operationType.code);
        logInfo.setDescription(log.title() + operationType.description);

        Map<String, Object> paramMap = new LinkedHashMap<>();
        String[] excludeRecordParam = log.excludeRecordParam();
        for (String expression : excludeRecordParam) {
            parseExpression(paramNames, args, expression);
        }
        for (int i = 0; i < paramNames.length; i++) {
            String paramName = paramNames[i];
            Object paramVal = args[i];
            if (filterParam(paramVal)) {
                continue;
            }
            paramMap.put(paramName, paramVal);
        }

        String json = objectMapper.writer().writeValueAsString(paramMap);
        logInfo.setParams(json);

        logInfo.setRequestUrl(requestUrl);
        logInfo.setTimeConsuming(timeConsuming);
        Integer code = ReturnCode.SUCCESS.code;
        if (result instanceof ResultData) {
            code = ((ResultData) result).getCode();
        } else if (result instanceof TableDataInfo) {
            code = ((TableDataInfo) result).getCode();
        }
        logInfo.setReturnCode(code);

        logInfoService.save(logInfo);
    }

    /**
     * 过滤参数
     *
     * @param paramVal 参数值
     * @return boolean
     */
    private boolean filterParam(Object paramVal) {
        return paramVal instanceof ServletResponse || paramVal instanceof ServletRequest;
    }

    /**
     * 解析 SpEL 表达式将对应的值设置为null
     *
     * @param paramNames 参数名称
     * @param params     参数
     * @param expression SpEL表达式
     */
    private void parseExpression(String[] paramNames, Object[] params, String expression) {
        if (!expression.contains(CALL_SYMBOL)) {
            manualNull(paramNames, params, expression);
            return;
        }
        EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
        for (int i = 0; i < paramNames.length; i++) {
            context.setVariable(paramNames[i], params[i]);
        }
        Expression ex = SP_EL_PARSER.parseExpression(expression);
        ex.setValue(context, null);
    }

    /**
     * 手动置null
     *
     * @param paramNames 参数名称
     * @param params     参数
     * @param expression SpEL表达式
     */
    private void manualNull(String[] paramNames, Object[] params, String expression) {
        int index = expression.indexOf("#");
        String setParamName = expression;
        if (index != -1) {
            setParamName = expression.substring(1);
        }
        for (int i = 0; i < paramNames.length; i++) {
            if (setParamName.equals(paramNames[i])) {
                params[i] = null;
                break;
            }
        }
    }

}

/**
 * 用户dto
 *
 * @author SoulBGM
 * @date 2022/11/22 09:34
 */
class UserDto {
    /**
     * 用户ID
     */
    private Long userId = 0L;
    /**
     * 昵称
     */
    private String nickname = "匿名用户";

    public UserDto() {
    }

    public UserDto(Long userId, String nickname) {
        this.userId = userId;
        this.nickname = nickname;
    }

    public Long getUserId() {
        return userId;
    }

    public String getNickname() {
        return nickname;
    }
}
